﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace V32Assembler
{
    public enum OpCode
    {
        LD,
        ADD,
        SUB,
        MUL,
        DIV,
        MOD,
        AND,
        OR,
        XOR,
        LSH,
        RSH,
        INV,
        NEG,
        CLR,
        CMP,
        JMP,
        JE,
        JNE,
        JG,
        JL,
        JLE,
        JGE,
        INT,
        IRET,

        DotDATA,
        DotDATABLOCK
    }
    public class Command
    {
        public static readonly Command Null = new Command() { OpCode = byte.MaxValue, Arg1 = null, Arg2 = null, HasNoArgs = true, HasTwoArgs = true };

        public byte OpCode { get; private set; }
        public bool HasNoArgs { get; private set; }
        public bool HasTwoArgs { get; private set; }

        public Argument Arg1 { get; private set; }
        public Argument Arg2 { get; private set; }

        public Command()
        {
            OpCode = 0;
            HasNoArgs = true;
            HasTwoArgs = false;
            Arg1 = Arg2 = null;
        }

        private Command(byte opCode)
        {
            OpCode = opCode;
            HasNoArgs = true;
            HasTwoArgs = false;
        }
        private Command(byte opCode, Argument arg)
        {
            OpCode = opCode;
            HasNoArgs = false;
            HasTwoArgs = false;
            Arg1 = arg;
        }
        private Command(byte opCode, Argument arg1, Argument arg2)
        {
            OpCode = opCode;
            HasNoArgs = false;
            HasTwoArgs = true;
            Arg1 = arg1;
            Arg2 = arg2;
        }

        public virtual byte[] ToBytes()
        {
            if (HasNoArgs)
                return new byte[] { OpCode };

            byte byte1 = OpCode;

            ArgInfo arg1 = Arg1.GetArgType();
            ArgInfo arg2 = HasTwoArgs ? Arg2.GetArgType() : null;

            if (Arg1 is DestArg && Arg2 is SourceArg)
                Argument.InferIndirectionSizes(Arg1 as DestArg, Arg2 as SourceArg);

            ArgSize? indSize1 = Arg1 is SourceArg
                ? ((SourceArg)Arg1).IndirectionSize
                : ((DestArg)Arg1).IndirectionSize;
            ArgSize? indSize2 = Arg2 == null ? null : (Arg2 is SourceArg
                ? ((SourceArg)Arg2).IndirectionSize
                : ((DestArg)Arg2).IndirectionSize);

            if (!indSize1.HasValue || indSize1.Value == ArgSize.Word)
                byte1 |= 1 << 6;
            if (!indSize2.HasValue || indSize2.Value == ArgSize.Word)
                byte1 |= 1 << 7;

            byte byte2 = arg1.TypeByte;
            if (HasTwoArgs) byte2 |= (byte)(arg2.TypeByte << 2);
            if (arg1.Relative) byte2 |= 1 << 4;
            if (HasTwoArgs && arg2.Relative) byte2 |= 1 << 5;
            if (arg1.Relative && arg1.Offset.ConstSize == ArgSize.Word) byte2 |= 1 << 6;
            if (HasTwoArgs && arg2.Relative && arg2.Offset.ConstSize == ArgSize.Word) byte2 |= 1 << 7;

            List<byte> bytes = new List<byte>() { byte1, byte2 };

            bytes.AddRange(Arg1.GetArgData());
            if (HasTwoArgs) bytes.AddRange(Arg2.GetArgData());

            return bytes.ToArray();
        }
        public virtual uint GetArg1Offset()
        {
            return 2;
        }
        public virtual uint GetArg2Offset()
        {
            return GetArg1Offset() + (Arg1 != null ? Arg1.GetArgSize() : 0);
        }

        private static bool InDataSect = false;
        public static PartialCommand FromLine(string line, Dictionary<string, int> labelLines)
        {
            string[] parts = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            if (parts[0].ToLower() == ".datasect")
            {
                if (parts.Length > 1) throw new Error(".datasect takes no arguments.");
                if (InDataSect) throw new Error("Cannot have multiple data sections.");
                InDataSect = true;
                throw new Exception(".datasect");
            }
            if (parts[0].StartsWith("."))
                return parseSpecial(line, labelLines);

            if (InDataSect) new Error("Cannot have normal opcodes after the start of the data section.");

            //if (parts.Length <= 1)
            //    throw new Error("A command must take at least one argument.");

            OpCode opcode;
            try
            {
                opcode = (OpCode)(Enum.Parse(typeof(OpCode), parts[0], true));
            }
            catch (Exception)
            {
                throw new Error("Unknown opcode '" + parts[0] + "'.");
            }

            Tuple<Argument, string> arg1 = null, arg2 = null;
            bool setArg1 = false, setArg2 = false;
            switch (opcode)
            {
                case V32Assembler.OpCode.LD:
                case V32Assembler.OpCode.ADD:
                case V32Assembler.OpCode.SUB:
                case V32Assembler.OpCode.MUL:
                case V32Assembler.OpCode.DIV:
                case V32Assembler.OpCode.MOD:
                case V32Assembler.OpCode.AND:
                case V32Assembler.OpCode.OR:
                case V32Assembler.OpCode.XOR:
                case V32Assembler.OpCode.LSH:
                case V32Assembler.OpCode.RSH:
                    if (parts.Length != 3)
                        throw new Error("Opcode '" + parts[0] + "' takes two arguments.");
                    arg1 = DestArg.FromString(parts[1], labelLines);
                    arg2 = SourceArg.FromString(parts[2], labelLines);
                    setArg1 = setArg2 = true;
                    break;
                case V32Assembler.OpCode.INV:
                case V32Assembler.OpCode.NEG:
                case V32Assembler.OpCode.CLR:
                    if (parts.Length != 2)
                        throw new Error("Opcode '" + parts[0] + "' takes one argument.");
                    arg1 = DestArg.FromString(parts[1], labelLines);
                    setArg1 = true;
                    break;
                case V32Assembler.OpCode.CMP:
                    if (parts.Length != 3)
                        throw new Error("Opcode '" + parts[0] + "' takes two arguments.");
                    arg1 = SourceArg.FromString(parts[1], labelLines);
                    arg2 = SourceArg.FromString(parts[2], labelLines);
                    setArg1 = setArg2 = true;
                    break;
                case V32Assembler.OpCode.JMP:
                case V32Assembler.OpCode.JE:
                case V32Assembler.OpCode.JNE:
                case V32Assembler.OpCode.JG:
                case V32Assembler.OpCode.JL:
                case V32Assembler.OpCode.JLE:
                case V32Assembler.OpCode.JGE:
                case V32Assembler.OpCode.INT:
                    if (parts.Length != 2)
                        throw new Error("Opcode '" + parts[0] + "' takes one argument.");
                    arg1 = SourceArg.FromString(parts[1], labelLines);
                    setArg1 = true;
                    break;
                case V32Assembler.OpCode.IRET:
                    if (parts.Length != 1)
                        throw new Error("Opcode '" + parts[0] + "' takes no arguments.");
                    break;
                default: throw new Exception("Forgot a case in Command.fromLine(string).");
            }

            if (InDataSect) return null;

            if ((setArg1 && arg1 == null) || (setArg2 && arg2 == null))
                return null;

            Command command;
            if (arg1 == null)
                command = new Command((byte)opcode);
            else if (arg2 == null)
                command = new Command((byte)opcode, arg1.Item1);
            else command = new Command((byte)opcode, arg1.Item1, arg2.Item1);
            string arg1Label = arg1 != null ? arg1.Item2 : "",
                   arg2Label = arg2 != null ? arg2.Item2 : "";

            return new PartialCommand(command, arg1Label, arg2Label);
        }

        private static PartialCommand parseSpecial(string line, Dictionary<string, int> labelLines)
        {
            string[] parts = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

            if (parts[0].ToLower() == ".data")
            {
                if (parts.Length <= 1) throw new Error("The .data command takes one argument.");

                OffsetNumber constVal = parts[1].ParseOffsetNumber(labelLines);
                if (constVal != null)
                    return new PartialCommand(new DataCommand(constVal), "", "");

                // not a number, so try to parse a label.
                line = line.Substring(parts[0].Length).Trim();
                if (labelLines.ContainsKey(line))
                    return new PartialCommand(new DataCommand(new OffsetNumber(0, false)), line, "");
                
                // not a number, so try to parse a string.
                return new PartialCommand(new DataCommand(line.ParseString()), "", "");
            }
            else if (parts[0].ToLower() == ".datablock")
            {
                if (parts.Length <= 1) throw new Error("The .datablock command takes one argument.");

                BaseNumber constVal = parts[1].ParseBaseNumber(labelLines);
                if (constVal != null)
                    return new PartialCommand(new DataBlockCommand(constVal.WordVal), "", "");
                throw new Error("Could not parse '" + parts[1] + "' as a number.");
            }
            else throw new Error("Unknown command '" + parts[0] + "'.");
        }

        public override string ToString()
        {
            return "[" + ((V32Assembler.OpCode)OpCode).ToString() + (Arg1 != null ? (" " + Arg1.ToString() + (Arg2 != null ? " " + Arg2.ToString() : "")) : "") + "]";
        }
    }

    public enum DataType { String, Int }
    public class DataCommand : Command
    {
        public DataType Type { get; private set; }

        public string StringVal { get; private set; }
        public OffsetNumber NumVal { get; private set; }

        private DataCommand(DataType type, string stringVal, OffsetNumber numVal)
        {
            Type = type;
            StringVal = stringVal;
            NumVal = numVal;
        }
        public DataCommand(string str)
            : this(DataType.String, str, new OffsetNumber()) { }
        public DataCommand(OffsetNumber numVal)
            : this(DataType.Int, "", numVal) { }

        public override byte[] ToBytes()
        {
            if (Type == DataType.Int)
                return NumVal.Bytes;

            List<byte> strBytes = new List<byte>();
            byte[] bytes = new System.Text.ASCIIEncoding().GetBytes(StringVal);
            strBytes.AddRange(((uint)bytes.Length).ToBytes());
            strBytes.AddRange(bytes);
            return strBytes.ToArray();
        }
        public override uint GetArg1Offset()
        {
            return 0;
        }
        public override uint GetArg2Offset()
        {
            return GetArg1Offset();
        }

        public override string ToString()
        {
            return ".data " + (Type == DataType.String ? ("\"" + StringVal + "\"") : NumVal.ToString());
        }
    }
    public class DataBlockCommand : Command
    {
        public uint BlockSize { get; private set; }

        public DataBlockCommand(uint blockSize)
        {
            BlockSize = blockSize;
        }

        public override byte[] ToBytes()
        {
            return new byte[BlockSize];
        }

        public override string ToString()
        {
            return ".datablock " + BlockSize;
        }
    }

    public class MacroCommand : Command
    {
        public Macro Macro { get; private set; }
        public string[] Args { get; private set; }

        public MacroCommand(Macro macro, string[] args)
        {
            Macro = macro;
            Args = args;
        }
    }
}
